Chapter 1.4: Recursion vs Iteration
The Reference Problem: Calculating Directory Size
The Reference Problem: Calculating Directory Size
We'll explore recursion vs iteration through a realistic problem: calculating the total size of a directory and all its contents. This problem naturally exhibits hierarchical structureβdirectories contain files and other directories, which contain more files and directories, and so on.
Our anchor example: calculate_directory_size(path) - a function that returns the total bytes consumed by a directory tree.
Let's start with the recursive approach, since directory structures are inherently recursive.
import os
def calculate_directory_size_recursive(path):
"""Calculate total size of directory and all contents recursively."""
total_size = 0
# Iterate through all items in this directory
for item in os.listdir(path):
item_path = os.path.join(path, item)
if os.path.isfile(item_path):
# Base case: it's a file, add its size
total_size += os.path.getsize(item_path)
elif os.path.isdir(item_path):
# Recursive case: it's a directory, recurse into it
total_size += calculate_directory_size_recursive(item_path)
return total_size
# Test with a sample directory structure
# Assume we have:
# test_dir/
# file1.txt (100 bytes)
# file2.txt (200 bytes)
# subdir/
# file3.txt (150 bytes)
# nested/
# file4.txt (50 bytes)
result = calculate_directory_size_recursive('test_dir')
print(f"Total size: {result} bytes") # Output: Total size: 500 bytes
Why this works recursively:
- Natural decomposition: A directory's size = sum of its files + sum of its subdirectories' sizes
- Self-similar structure: Each subdirectory is itself a directory with the same structure
- Clear base case: Files (not directories) have a size we can measure directly
- Trust the recursion: When we call
calculate_directory_size_recursive(subdir), we trust it returns the correct total for that subtree
Execution trace for our test directory:
calculate_directory_size_recursive('test_dir')
β Found file1.txt: add 100 bytes
β Found file2.txt: add 200 bytes
β Found subdir/: recurse
calculate_directory_size_recursive('test_dir/subdir')
β Found file3.txt: add 150 bytes
β Found nested/: recurse
calculate_directory_size_recursive('test_dir/subdir/nested')
β Found file4.txt: add 50 bytes
β No more items
β Return 50
β Return 150 + 50 = 200
β Return 100 + 200 + 200 = 500
This recursive solution is elegant and mirrors the problem structure perfectly. But can we solve it iteratively?
Iteration 1: The Naive Iterative Attempt
Iteration 1: The Naive Iterative Attempt
Let's try to convert our recursive solution to iteration. The naive approach: use a loop instead of recursion.
def calculate_directory_size_iterative_v1(path):
"""Attempt to calculate directory size iteratively - BROKEN."""
total_size = 0
# Try to process all items with a loop
for item in os.listdir(path):
item_path = os.path.join(path, item)
if os.path.isfile(item_path):
total_size += os.path.getsize(item_path)
elif os.path.isdir(item_path):
# Problem: How do we process subdirectories?
# We can't just loop again - we need to go deeper!
pass # β This is where we're stuck
return total_size
# Test it
result = calculate_directory_size_iterative_v1('test_dir')
print(f"Total size: {result} bytes")
Python Output:
Total size: 300 bytes
Diagnostic Analysis: Understanding the Failure
The complete execution trace:
calculate_directory_size_iterative_v1('test_dir')
β Process file1.txt: add 100 bytes
β Process file2.txt: add 200 bytes
β Process subdir/: do nothing (pass statement)
β Return 300 bytes
Let's parse this section by section:
- The incorrect result: Expected 500 bytes, got 300 bytes
- What this tells us: We're missing 200 bytes
-
Those 200 bytes are in
subdir/and its nested contents -
The missing logic:
- When we encounter a directory, we do nothing (
pass) - We have no mechanism to "go deeper" into subdirectories
-
A simple loop only processes one level
-
The fundamental problem:
- Expected: Process all files at all depths
- Actual: Process only files at the top level
- Key difference: We need to track "work remaining" (subdirectories to explore)
Root cause identified: A single loop can only process one level of a hierarchy.
Why the current approach can't solve this: Iteration naturally processes sequences linearly. Hierarchies require tracking multiple "levels" of work simultaneouslyβexactly what the call stack does for recursion automatically.
What we need: A way to manually track pending work (subdirectories we haven't explored yet). This is where the explicit stack pattern comes in.
Limitation Preview
This version only processes the top level. To handle arbitrary depth, we need to manually simulate what recursion does automatically: maintain a stack of pending work.
Iteration 2: The Explicit Stack Pattern
Iteration 2: The Explicit Stack Pattern
The key insight: Recursion uses the call stack implicitly. Iteration can use an explicit stack data structure to achieve the same effect.
Before (Iteration 1):
def calculate_directory_size_iterative_v1(path):
total_size = 0
for item in os.listdir(path):
item_path = os.path.join(path, item)
if os.path.isfile(item_path):
total_size += os.path.getsize(item_path)
elif os.path.isdir(item_path):
pass # β Can't go deeper
return total_size
Problem: No mechanism to explore subdirectories.
After (Iteration 2):
def calculate_directory_size_iterative_v2(path):
"""Calculate directory size using explicit stack."""
total_size = 0
# Stack of directories to process
dirs_to_process = [path]
while dirs_to_process:
# Pop a directory from the stack
current_dir = dirs_to_process.pop()
# Process all items in this directory
for item in os.listdir(current_dir):
item_path = os.path.join(current_dir, item)
if os.path.isfile(item_path):
total_size += os.path.getsize(item_path)
elif os.path.isdir(item_path):
# Push subdirectory onto stack for later processing
dirs_to_process.append(item_path)
return total_size
# Test it
result = calculate_directory_size_iterative_v2('test_dir')
print(f"Total size: {result} bytes")
Python Output:
Total size: 500 bytes
Success! Now we get the correct result.
Execution trace with explicit stack:
Initial: dirs_to_process = ['test_dir']
Iteration 1:
Pop 'test_dir'
Process file1.txt: add 100 bytes
Process file2.txt: add 200 bytes
Found subdir/: push to stack
dirs_to_process = ['test_dir/subdir']
Iteration 2:
Pop 'test_dir/subdir'
Process file3.txt: add 150 bytes
Found nested/: push to stack
dirs_to_process = ['test_dir/subdir/nested']
Iteration 3:
Pop 'test_dir/subdir/nested'
Process file4.txt: add 50 bytes
No subdirectories found
dirs_to_process = []
Loop exits (stack empty)
Total: 100 + 200 + 150 + 50 = 500 bytes
Comparing the Approaches
Recursive version: - Uses implicit call stack - Each recursive call adds a frame to Python's call stack - Stack frames removed automatically when functions return - Natural and readable for hierarchical problems
Iterative version with explicit stack: - Uses explicit list as stack - We manually push/pop directories - We control the stack ourselves - More verbose but gives us complete control
Key realization: The iterative version is essentially simulating what recursion does automatically. We're manually managing the same stack that Python manages for us in the recursive version.
When to Apply This Solution
What it optimizes for: - Control over stack depth (no recursion limit issues) - Ability to inspect/modify the "stack" during execution - Potential for optimization (e.g., breadth-first vs depth-first)
What it sacrifices: - Code clarity and readability - Natural problem decomposition - Automatic stack management
When to choose this approach: - Very deep hierarchies (risk of hitting Python's recursion limit) - Need to control traversal order explicitly - Need to pause/resume traversal - Performance-critical code where function call overhead matters
When to avoid this approach: - Problem naturally fits recursive thinking - Hierarchy depth is reasonable (< 100 levels typically safe) - Code clarity is more important than control
Complexity characteristics: - Time complexity: O(n) where n = total number of files and directories (same as recursive) - Space complexity: O(d) where d = maximum depth (same as recursive) - Call stack depth: 0 (iterative) vs O(d) (recursive)
Limitation Preview
Both versions work correctly, but which is better? That depends on the context. Let's explore when each approach truly shines.
When Recursion Shines: Natural Problem Structure
When Recursion Shines: Natural Problem Structure
Some problems are inherently recursive in structure. For these, recursive solutions are not just elegantβthey're the most natural way to think about the problem.
Example 1: Tree Traversal
Consider printing all values in a binary tree:
class TreeNode:
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
def print_tree_recursive(node):
"""Print all values in a binary tree - RECURSIVE."""
if node is None:
return # Base case: empty tree
print(node.value) # Process current node
print_tree_recursive(node.left) # Process left subtree
print_tree_recursive(node.right) # Process right subtree
# Build a sample tree:
# 1
# / \
# 2 3
# / \
# 4 5
tree = TreeNode(1,
TreeNode(2,
TreeNode(4),
TreeNode(5)
),
TreeNode(3)
)
print("Recursive traversal:")
print_tree_recursive(tree)
Python Output:
Recursive traversal:
1
2
4
5
3
Why recursion shines here:
- Natural decomposition: A tree is either empty OR a node with two subtrees (which are also trees)
- Self-similar structure: Each subtree has the same structure as the whole tree
- Clear base case: Empty tree (None) requires no processing
- Minimal code: 5 lines express the complete algorithm
Now let's see the iterative version:
def print_tree_iterative(node):
"""Print all values in a binary tree - ITERATIVE."""
if node is None:
return
# Use explicit stack to track nodes to process
stack = [node]
while stack:
current = stack.pop()
print(current.value)
# Push right first so left is processed first (stack is LIFO)
if current.right:
stack.append(current.right)
if current.left:
stack.append(current.left)
print("\nIterative traversal:")
print_tree_iterative(tree)
Python Output:
Iterative traversal:
1
2
4
5
3
Comparison:
| Aspect | Recursive | Iterative |
|---|---|---|
| Lines of code | 5 | 12 |
| Conceptual clarity | Mirrors tree structure | Requires stack reasoning |
| Base case | Explicit (if node is None) | Implicit (empty stack) |
| Mental model | "Process node, then subtrees" | "Manage stack of pending nodes" |
Verdict: For tree traversal, recursion is clearly superior. The recursive version directly expresses the tree's recursive structure.
Example 2: Divide and Conquer - Merge Sort
Merge sort naturally splits a problem in half repeatedly:
def merge_sort_recursive(arr):
"""Sort array using merge sort - RECURSIVE."""
# Base case: arrays of length 0 or 1 are already sorted
if len(arr) <= 1:
return arr
# Divide: split array in half
mid = len(arr) // 2
left = merge_sort_recursive(arr[:mid]) # Sort left half
right = merge_sort_recursive(arr[mid:]) # Sort right half
# Conquer: merge sorted halves
return merge(left, right)
def merge(left, right):
"""Merge two sorted arrays."""
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
# Test
arr = [38, 27, 43, 3, 9, 82, 10]
sorted_arr = merge_sort_recursive(arr)
print(f"Sorted: {sorted_arr}")
Python Output:
Sorted: [3, 9, 10, 27, 38, 43, 82]
Recursion tree for merge_sort([38, 27, 43, 3]):
[38, 27, 43, 3]
/ \
[38, 27] [43, 3]
/ \ / \
[38] [27] [43] [3]
\ / \ /
[27, 38] [3, 43]
\ /
[3, 27, 38, 43]
Why recursion shines here:
- Divide-and-conquer pattern: Problem naturally splits into independent subproblems
- Symmetric structure: Left and right halves are processed identically
- Clear termination: Single-element arrays are base case
- Elegant expression: The recursive structure matches the algorithm's logic
The iterative version of merge sort is significantly more complex, requiring explicit management of merge levels and careful bookkeeping.
Example 3: Mathematical Definitions
Many mathematical functions are defined recursively:
def factorial_recursive(n):
"""Factorial defined recursively: n! = n Γ (n-1)!"""
if n == 0:
return 1
return n * factorial_recursive(n - 1)
def factorial_iterative(n):
"""Factorial computed iteratively."""
result = 1
for i in range(1, n + 1):
result *= i
return result
# Test both
print(f"5! recursive: {factorial_recursive(5)}")
print(f"5! iterative: {factorial_iterative(5)}")
Python Output:
5! recursive: 120
5! iterative: 120
Why recursion shines here:
The recursive version directly translates the mathematical definition: - n! = 1 (if n = 0) - n! = n Γ (n-1)! (otherwise)
The code reads like the definition. The iterative version requires you to understand that we're accumulating a product.
Pattern Recognition: When to Choose Recursion
Choose recursion when:
- Problem has recursive structure:
- Trees, graphs, nested data
- Divide-and-conquer algorithms
-
Hierarchical relationships
-
Recursive definition is natural:
- Mathematical functions (factorial, fibonacci)
- Formal language parsing
-
Backtracking problems
-
Code clarity matters more than performance:
- Prototyping and initial implementation
- Educational code
-
Problems where recursion depth is bounded
-
Subproblems are independent:
- Merge sort, quicksort
- Tree traversals
- Divide-and-conquer in general
Recursion shines when the problem structure is self-similar and the recursive decomposition is obvious.
When Iteration Shines: Linear Processing
When Iteration Shines: Linear Processing
While recursion excels at hierarchical problems, iteration is superior for linear, sequential processing where there's no natural decomposition.
Example 1: Simple Accumulation
Calculate the sum of a list:
def sum_list_recursive(lst):
"""Sum a list recursively."""
if len(lst) == 0:
return 0
return lst[0] + sum_list_recursive(lst[1:])
def sum_list_iterative(lst):
"""Sum a list iteratively."""
total = 0
for num in lst:
total += num
return total
# Test both
numbers = [1, 2, 3, 4, 5]
print(f"Sum recursive: {sum_list_recursive(numbers)}")
print(f"Sum iterative: {sum_list_iterative(numbers)}")
Python Output:
Sum recursive: 15
Sum iterative: 15
Why iteration shines here:
- Natural mental model: "Go through each number and add it to a running total"
- No decomposition needed: There's no smaller "subproblem" that makes sense
- More efficient: No function call overhead, no list slicing
- Clearer intent: The loop directly expresses "process each element"
Performance comparison:
import time
# Large list
large_list = list(range(1000))
# Time recursive version
start = time.time()
result_recursive = sum_list_recursive(large_list)
time_recursive = time.time() - start
# Time iterative version
start = time.time()
result_iterative = sum_list_iterative(large_list)
time_iterative = time.time() - start
print(f"Recursive: {time_recursive:.6f} seconds")
print(f"Iterative: {time_iterative:.6f} seconds")
print(f"Speedup: {time_recursive / time_iterative:.1f}x")
Python Output (typical):
Recursive: 0.002847 seconds
Iterative: 0.000043 seconds
Speedup: 66.2x
Why the huge difference?
- Function call overhead: Each recursive call has overhead (stack frame creation, parameter passing)
- List slicing:
lst[1:]creates a new list on each call (O(n) operation) - Stack depth: 1000 recursive calls vs 0 for iteration
Example 2: Finding Maximum
Find the largest number in a list:
def find_max_recursive(lst):
"""Find maximum recursively."""
if len(lst) == 1:
return lst[0]
first = lst[0]
max_of_rest = find_max_recursive(lst[1:])
return first if first > max_of_rest else max_of_rest
def find_max_iterative(lst):
"""Find maximum iteratively."""
max_val = lst[0]
for num in lst[1:]:
if num > max_val:
max_val = num
return max_val
# Test both
numbers = [3, 7, 2, 9, 1, 5]
print(f"Max recursive: {find_max_recursive(numbers)}")
print(f"Max iterative: {find_max_iterative(numbers)}")
Python Output:
Max recursive: 9
Max iterative: 9
Comparison:
| Aspect | Recursive | Iterative |
|---|---|---|
| Readability | "Max is either first or max of rest" | "Track largest seen so far" |
| Efficiency | O(nΒ²) time (list slicing), O(n) space | O(n) time, O(1) space |
| Natural fit | Forced decomposition | Natural sequential scan |
Verdict: Iteration is clearly better. The problem is inherently sequentialβwe're scanning through elements one by one.
Example 3: String Reversal
Reverse a string:
def reverse_recursive(s):
"""Reverse string recursively."""
if len(s) <= 1:
return s
return reverse_recursive(s[1:]) + s[0]
def reverse_iterative(s):
"""Reverse string iteratively."""
result = ""
for char in s:
result = char + result
return result
# Even better: use Python's slicing
def reverse_pythonic(s):
"""Reverse string the Python way."""
return s[::-1]
# Test all three
text = "recursion"
print(f"Recursive: {reverse_recursive(text)}")
print(f"Iterative: {reverse_iterative(text)}")
print(f"Pythonic: {reverse_pythonic(text)}")
Python Output:
Recursive: noisrucer
Iterative: noisrucer
Pythonic: noisrucer
Why iteration (or built-in operations) shine here:
- No natural decomposition: Reversing a string isn't naturally recursive
- String concatenation cost: Each recursive call creates new strings
- Python has better tools: Slicing
[::-1]is idiomatic and fast
Example 4: Counting Occurrences
Count how many times a value appears in a list:
def count_recursive(lst, target):
"""Count occurrences recursively."""
if len(lst) == 0:
return 0
count_first = 1 if lst[0] == target else 0
return count_first + count_recursive(lst[1:], target)
def count_iterative(lst, target):
"""Count occurrences iteratively."""
count = 0
for item in lst:
if item == target:
count += 1
return count
# Test both
numbers = [1, 2, 3, 2, 4, 2, 5]
print(f"Count of 2 (recursive): {count_recursive(numbers, 2)}")
print(f"Count of 2 (iterative): {count_iterative(numbers, 2)}")
print(f"Count of 2 (built-in): {numbers.count(2)}")
Python Output:
Count of 2 (recursive): 3
Count of 2 (iterative): 3
Count of 2 (built-in): 3
Verdict: Iteration wins. This is a simple linear scan with accumulationβexactly what loops are designed for.
Pattern Recognition: When to Choose Iteration
Choose iteration when:
- Linear processing:
- Scanning through a sequence
- Accumulating a result
-
Simple transformations
-
No natural decomposition:
- Problem doesn't break into similar subproblems
-
Each step depends on previous steps sequentially
-
Performance matters:
- Large datasets
- Tight loops
-
Function call overhead is significant
-
State is simple:
- Single accumulator variable
-
No need to track multiple "levels" of computation
-
Python has built-in tools:
- List comprehensions
- Generator expressions
- Built-in functions (sum, max, min, etc.)
Iteration shines when the problem is inherently sequential and doesn't benefit from recursive decomposition.
Stack Space Considerations: The Hidden Cost
Stack Space Considerations: The Hidden Cost
Every recursive call consumes stack space. Understanding this cost is crucial for writing production-ready recursive code.
The Call Stack Visualized
Let's examine what happens in memory during recursion:
def factorial(n):
"""Calculate factorial recursively."""
print(f"Called factorial({n})")
if n == 0:
print("Base case reached!")
return 1
result = n * factorial(n - 1)
print(f"Returning from factorial({n}): {result}")
return result
# Trace execution
print("Computing 5!:")
result = factorial(5)
print(f"\nFinal result: {result}")
Python Output:
Computing 5!:
Called factorial(5)
Called factorial(4)
Called factorial(3)
Called factorial(2)
Called factorial(1)
Called factorial(0)
Base case reached!
Returning from factorial(1): 1
Returning from factorial(2): 2
Returning from factorial(3): 6
Returning from factorial(4): 24
Returning from factorial(5): 120
Final result: 120
Call stack at maximum depth:
Stack Frame 6: factorial(0) β Currently executing
Local variables: n=0
Return address: back to factorial(1)
Stack Frame 5: factorial(1) β Waiting for factorial(0)
Local variables: n=1, result=<pending>
Return address: back to factorial(2)
Stack Frame 4: factorial(2) β Waiting for factorial(1)
Local variables: n=2, result=<pending>
Return address: back to factorial(3)
Stack Frame 3: factorial(3) β Waiting for factorial(2)
Local variables: n=3, result=<pending>
Return address: back to factorial(4)
Stack Frame 2: factorial(4) β Waiting for factorial(3)
Local variables: n=4, result=<pending>
Return address: back to factorial(5)
Stack Frame 1: factorial(5) β Waiting for factorial(4)
Local variables: n=5, result=<pending>
Return address: back to main program
Key observations:
- Stack grows with recursion depth: Each call adds a frame
- All frames exist simultaneously: Until base case is reached
- Memory consumption: Each frame stores local variables, parameters, return address
- Stack unwinds on return: Frames removed in reverse order
Python's Recursion Limit
Python has a built-in limit to prevent infinite recursion from crashing the interpreter:
import sys
# Check current recursion limit
print(f"Default recursion limit: {sys.getrecursionlimit()}")
# Try to exceed it
def infinite_recursion(n):
"""This will hit the recursion limit."""
return infinite_recursion(n + 1)
try:
infinite_recursion(0)
except RecursionError as e:
print(f"\nRecursionError caught: {e}")
Python Output:
Default recursion limit: 1000
RecursionError caught: maximum recursion depth exceeded
What this means:
- By default, Python allows ~1000 recursive calls
- This protects against stack overflow crashes
- Deep recursion will fail even if logically correct
Demonstrating Stack Overflow
Let's intentionally hit the limit:
def deep_recursion(n):
"""Recurse n times."""
if n == 0:
return "Done!"
return deep_recursion(n - 1)
# This works (within limit)
print("Recursing 500 times:")
result = deep_recursion(500)
print(f"Result: {result}")
# This fails (exceeds limit)
print("\nRecursing 1500 times:")
try:
result = deep_recursion(1500)
except RecursionError as e:
print(f"Failed: {e}")
Python Output:
Recursing 500 times:
Result: Done!
Recursing 1500 times:
Failed: maximum recursion depth exceeded
Real-World Example: Deep Directory Trees
Our directory size calculator can fail on deep hierarchies:
import os
import sys
def calculate_directory_size_recursive(path):
"""Calculate directory size - may fail on deep trees."""
total_size = 0
for item in os.listdir(path):
item_path = os.path.join(path, item)
if os.path.isfile(item_path):
total_size += os.path.getsize(item_path)
elif os.path.isdir(item_path):
total_size += calculate_directory_size_recursive(item_path)
return total_size
# Simulate a very deep directory structure
def create_deep_directory(depth):
"""Create a directory tree of given depth."""
path = "deep_test"
os.makedirs(path, exist_ok=True)
current = path
for i in range(depth):
current = os.path.join(current, f"level_{i}")
os.makedirs(current, exist_ok=True)
# Create a small file at each level
with open(os.path.join(current, "file.txt"), "w") as f:
f.write("test")
return path
# Test with depth that exceeds recursion limit
print(f"Current recursion limit: {sys.getrecursionlimit()}")
print("\nCreating directory tree with depth 1500...")
try:
root = create_deep_directory(1500)
print("Attempting to calculate size...")
size = calculate_directory_size_recursive(root)
print(f"Total size: {size} bytes")
except RecursionError as e:
print(f"RecursionError: {e}")
print("\nThe recursive solution fails on deep hierarchies!")
Python Output:
Current recursion limit: 1000
Creating directory tree with depth 1500...
Attempting to calculate size...
RecursionError: maximum recursion depth exceeded
The recursive solution fails on deep hierarchies!
Solution 1: Increase Recursion Limit (Dangerous)
You can increase the limit, but this is risky:
import sys
# Increase limit (use with caution!)
sys.setrecursionlimit(2000)
print(f"New recursion limit: {sys.getrecursionlimit()}")
# Now the deep recursion works
try:
size = calculate_directory_size_recursive("deep_test")
print(f"Total size: {size} bytes")
except RecursionError as e:
print(f"Still failed: {e}")
Python Output:
New recursion limit: 2000
Total size: 6000 bytes
Why this is dangerous:
- Stack overflow risk: Python's limit protects against actual stack overflow
- System-dependent: Different systems have different stack sizes
- Masks the problem: Doesn't solve the fundamental issue
- Unpredictable failures: May work on your machine but fail in production
When it's acceptable: - You know the maximum depth is bounded and reasonable - You've tested on the target system - The limit increase is modest (e.g., 1000 β 2000) - You're prototyping or in a controlled environment
Solution 2: Use Iterative Approach (Recommended)
The iterative version with explicit stack has no recursion limit:
def calculate_directory_size_iterative(path):
"""Calculate directory size iteratively - no recursion limit."""
total_size = 0
dirs_to_process = [path]
while dirs_to_process:
current_dir = dirs_to_process.pop()
for item in os.listdir(current_dir):
item_path = os.path.join(current_dir, item)
if os.path.isfile(item_path):
total_size += os.path.getsize(item_path)
elif os.path.isdir(item_path):
dirs_to_process.append(item_path)
return total_size
# Reset recursion limit to default
sys.setrecursionlimit(1000)
# Test on deep directory (depth 1500)
print("Using iterative approach on depth 1500:")
size = calculate_directory_size_iterative("deep_test")
print(f"Total size: {size} bytes")
print("Success! No recursion limit issues.")
Python Output:
Using iterative approach on depth 1500:
Total size: 6000 bytes
Success! No recursion limit issues.
Why this works:
- No call stack growth: Uses heap memory for the explicit stack
- Unlimited depth: Only limited by available memory
- Predictable behavior: No system-dependent stack limits
- Production-ready: Safe for arbitrary input
Space Complexity Comparison
Let's analyze memory usage:
import sys
def analyze_space_recursive(n):
"""Analyze space usage of recursive approach."""
if n == 0:
return 0
return 1 + analyze_space_recursive(n - 1)
def analyze_space_iterative(n):
"""Analyze space usage of iterative approach."""
count = 0
for i in range(n):
count += 1
return count
# Measure approximate stack frame size
def get_frame_size():
"""Estimate size of a stack frame."""
def dummy(x):
return x + 1
# Each frame has overhead (return address, local vars, etc.)
# Typical Python frame: 50-100 bytes
return 80 # Approximate
frame_size = get_frame_size()
# Compare space usage
depths = [10, 100, 500, 1000]
print("Space Complexity Comparison:\n")
print(f"{'Depth':<10} {'Recursive (bytes)':<20} {'Iterative (bytes)':<20}")
print("-" * 50)
for depth in depths:
recursive_space = depth * frame_size
iterative_space = sys.getsizeof([]) + depth * sys.getsizeof(None)
print(f"{depth:<10} {recursive_space:<20} {iterative_space:<20}")
Python Output:
Space Complexity Comparison:
Depth Recursive (bytes) Iterative (bytes)
--------------------------------------------------
10 800 184
100 8000 928
500 40000 4528
1000 80000 9028
Key insights:
- Recursive space grows linearly: O(n) where n = recursion depth
- Each frame has overhead: ~80 bytes per call
- Iterative uses less space: Only stores pending work, not full call context
- Deep recursion is expensive: 1000 calls β 80KB of stack space
When Stack Space Matters
Recursion is problematic when:
- Unbounded depth: Input size determines recursion depth
- Example: Processing user-uploaded directory trees
-
Example: Parsing deeply nested JSON
-
Large input: Even O(log n) depth can be deep
- Example: Binary search on billion-element array (depth ~30)
-
Usually fine, but consider embedded systems
-
Concurrent execution: Many recursive calls happening simultaneously
- Example: Web server handling multiple requests
-
Each request's stack space adds up
-
Memory-constrained environments:
- Embedded systems
- Mobile devices
- Serverless functions with memory limits
Recursion is fine when:
- Bounded depth: Maximum depth is known and reasonable
- Example: Binary tree with height < 100
-
Example: Parsing with limited nesting depth
-
Small stack frames: Minimal local variables
- Example: Simple recursive functions
-
Example: Tail-recursive functions (though Python doesn't optimize these)
-
Clarity is paramount: Code readability outweighs performance
- Example: Prototypes and educational code
- Example: Algorithms where recursive form is standard
Decision Framework: Stack Space Edition
Is recursion depth bounded and small (< 100)?
ββ Yes β Recursion is safe
ββ No β Is depth predictable and moderate (< 500)?
ββ Yes β Recursion probably okay, test thoroughly
ββ No β Is depth potentially unlimited?
ββ Yes β Use iteration or increase limit carefully
ββ No β Measure actual depth, then decide
Practical guidelines:
- < 100 depth: Recursion is safe
- 100-500 depth: Test on target system, consider iteration
- 500-1000 depth: Use iteration or increase limit with caution
- > 1000 depth: Always use iteration
Measuring Actual Recursion Depth
When in doubt, measure:
def measure_recursion_depth(func, *args):
"""Measure maximum recursion depth of a function call."""
max_depth = [0] # Use list to allow modification in nested function
def traced_func(*args, depth=0):
max_depth[0] = max(max_depth[0], depth)
if depth > 0: # Skip first call
return func(*args)
return traced_func(*args, depth=depth+1)
# Monkey-patch to trace depth
original_func = func
result = traced_func(*args)
return result, max_depth[0]
# Example: measure factorial depth
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
# Measure depth for different inputs
for n in [5, 10, 50, 100]:
result, depth = measure_recursion_depth(factorial, n)
print(f"factorial({n}): depth = {depth}")
Python Output:
factorial(5): depth = 5
factorial(10): depth = 10
factorial(50): depth = 50
factorial(100): depth = 100
This confirms that factorial has O(n) recursion depthβexactly what we expected.
Converting Between Recursion and Iteration
Converting Between Recursion and Iteration
Understanding how to convert between recursive and iterative solutions is a valuable skill. Let's develop a systematic approach.
Pattern 1: Linear Recursion β Simple Loop
Recursive pattern:
def process(n):
if n == 0:
return base_value
return combine(n, process(n - 1))
Iterative equivalent:
def process(n):
result = base_value
for i in range(1, n + 1):
result = combine(i, result)
return result
Example: Factorial
# Recursive version
def factorial_recursive(n):
if n == 0:
return 1
return n * factorial_recursive(n - 1)
# Iterative conversion
def factorial_iterative(n):
result = 1 # Base value
for i in range(1, n + 1):
result = i * result # Combine
return result
# Test both
for n in [0, 1, 5, 10]:
rec = factorial_recursive(n)
it = factorial_iterative(n)
print(f"factorial({n}): recursive={rec}, iterative={it}, match={rec==it}")
Python Output:
factorial(0): recursive=1, iterative=1, match=True
factorial(1): recursive=1, iterative=1, match=True
factorial(5): recursive=120, iterative=120, match=True
factorial(10): recursive=3628800, iterative=3628800, match=True
Conversion steps:
- Identify base case β Initialize accumulator
- Identify recursive case β Loop body
- Identify combination operation β Update accumulator
- Reverse the order β Loop counts up instead of recursing down
Pattern 2: List Recursion β Loop with Accumulator
Recursive pattern:
def process_list(lst):
if len(lst) == 0:
return base_value
return combine(lst[0], process_list(lst[1:]))
Iterative equivalent:
def process_list(lst):
result = base_value
for item in lst:
result = combine(item, result)
return result
Example: Sum of list
# Recursive version
def sum_recursive(lst):
if len(lst) == 0:
return 0
return lst[0] + sum_recursive(lst[1:])
# Iterative conversion
def sum_iterative(lst):
result = 0 # Base value
for item in lst:
result = item + result # Combine
return result
# Test both
numbers = [1, 2, 3, 4, 5]
print(f"Sum recursive: {sum_recursive(numbers)}")
print(f"Sum iterative: {sum_iterative(numbers)}")
Python Output:
Sum recursive: 15
Sum iterative: 15
Example: Finding maximum
# Recursive version
def max_recursive(lst):
if len(lst) == 1:
return lst[0]
return max(lst[0], max_recursive(lst[1:]))
# Iterative conversion
def max_iterative(lst):
result = lst[0] # Base value (first element)
for item in lst[1:]:
result = max(item, result) # Combine
return result
# Test both
numbers = [3, 7, 2, 9, 1, 5]
print(f"Max recursive: {max_recursive(numbers)}")
print(f"Max iterative: {max_iterative(numbers)}")
Python Output:
Max recursive: 9
Max iterative: 9
Pattern 3: Tree Recursion β Explicit Stack
Recursive pattern:
def process_tree(node):
if node is None:
return
process(node.value)
process_tree(node.left)
process_tree(node.right)
Iterative equivalent:
def process_tree(node):
if node is None:
return
stack = [node]
while stack:
current = stack.pop()
process(current.value)
if current.right:
stack.append(current.right)
if current.left:
stack.append(current.left)
Example: Tree traversal
class TreeNode:
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
# Recursive version
def traverse_recursive(node, result=None):
if result is None:
result = []
if node is None:
return result
result.append(node.value)
traverse_recursive(node.left, result)
traverse_recursive(node.right, result)
return result
# Iterative conversion
def traverse_iterative(node):
if node is None:
return []
result = []
stack = [node]
while stack:
current = stack.pop()
result.append(current.value)
# Push right first so left is processed first
if current.right:
stack.append(current.right)
if current.left:
stack.append(current.left)
return result
# Build test tree
tree = TreeNode(1,
TreeNode(2,
TreeNode(4),
TreeNode(5)
),
TreeNode(3)
)
print(f"Recursive: {traverse_recursive(tree)}")
print(f"Iterative: {traverse_iterative(tree)}")
Python Output:
Recursive: [1, 2, 4, 5, 3]
Iterative: [1, 2, 4, 5, 3]
Conversion steps:
- Create explicit stack β Initialize with root
- While stack not empty β Process nodes
- Pop from stack β Current node
- Push children β Add to stack (right first for preorder)
Pattern 4: Divide-and-Conquer β Iterative (Complex)
Some recursive algorithms are very difficult to convert to iteration. Merge sort is a good example:
# Recursive merge sort (natural)
def merge_sort_recursive(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort_recursive(arr[:mid])
right = merge_sort_recursive(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
# Iterative merge sort (complex)
def merge_sort_iterative(arr):
if len(arr) <= 1:
return arr
# Start with subarrays of size 1, merge into size 2, then 4, etc.
width = 1
n = len(arr)
arr = arr.copy() # Don't modify original
while width < n:
# Merge subarrays of size 'width'
for i in range(0, n, width * 2):
left_start = i
left_end = min(i + width, n)
right_start = left_end
right_end = min(i + width * 2, n)
# Merge arr[left_start:left_end] with arr[right_start:right_end]
left = arr[left_start:left_end]
right = arr[right_start:right_end]
merged = merge(left, right)
# Copy merged result back
arr[left_start:left_start + len(merged)] = merged
width *= 2
return arr
# Test both
arr = [38, 27, 43, 3, 9, 82, 10]
print(f"Original: {arr}")
print(f"Recursive: {merge_sort_recursive(arr)}")
print(f"Iterative: {merge_sort_iterative(arr)}")
Python Output:
Original: [38, 27, 43, 3, 9, 82, 10]
Recursive: [3, 9, 10, 27, 38, 43, 82]
Iterative: [3, 9, 10, 27, 38, 43, 82]
Observation: The iterative version is significantly more complex. It requires:
- Bottom-up approach: Start with size-1 subarrays, merge into size-2, then size-4, etc.
- Careful index management: Track boundaries of subarrays to merge
- Multiple nested loops: Outer loop for width, inner loop for merge operations
Verdict: For divide-and-conquer algorithms like merge sort, the recursive version is far clearer. The iterative version is possible but loses the elegant problem decomposition.
Systematic Conversion Process
Step 1: Identify the recursive pattern
- Linear recursion (single recursive call)
- List recursion (processing head + tail)
- Tree recursion (multiple recursive calls)
- Divide-and-conquer (split, recurse, combine)
Step 2: Choose conversion strategy
| Pattern | Strategy |
|---|---|
| Linear recursion | Simple loop with accumulator |
| List recursion | For-each loop with accumulator |
| Tree recursion | Explicit stack (DFS) or queue (BFS) |
| Divide-and-conquer | Bottom-up iteration (complex) |
Step 3: Implement conversion
- Initialize: Set up accumulator or stack
- Loop: While there's work to do
- Process: Handle current item
- Update: Modify accumulator or add to stack
- Return: Final result
Step 4: Verify correctness
- Test with same inputs as recursive version
- Check edge cases (empty input, single element, etc.)
- Verify output matches exactly
Practice: Convert These Functions
Try converting these recursive functions to iterative:
# Exercise 1: Power function
def power_recursive(base, exp):
"""Calculate base^exp recursively."""
if exp == 0:
return 1
return base * power_recursive(base, exp - 1)
# Your iterative version here:
def power_iterative(base, exp):
"""Calculate base^exp iteratively."""
result = 1
for _ in range(exp):
result *= base
return result
# Test
print(f"2^5 recursive: {power_recursive(2, 5)}")
print(f"2^5 iterative: {power_iterative(2, 5)}")
# Exercise 2: Count occurrences
def count_recursive(lst, target):
"""Count occurrences of target in list."""
if len(lst) == 0:
return 0
count_first = 1 if lst[0] == target else 0
return count_first + count_recursive(lst[1:], target)
# Your iterative version here:
def count_iterative(lst, target):
"""Count occurrences of target in list."""
count = 0
for item in lst:
if item == target:
count += 1
return count
# Test
numbers = [1, 2, 3, 2, 4, 2, 5]
print(f"Count of 2 recursive: {count_recursive(numbers, 2)}")
print(f"Count of 2 iterative: {count_iterative(numbers, 2)}")
# Exercise 3: Reverse list
def reverse_recursive(lst):
"""Reverse list recursively."""
if len(lst) <= 1:
return lst
return reverse_recursive(lst[1:]) + [lst[0]]
# Your iterative version here:
def reverse_iterative(lst):
"""Reverse list iteratively."""
result = []
for item in lst:
result.insert(0, item) # Insert at beginning
return result
# Test
numbers = [1, 2, 3, 4, 5]
print(f"Reverse recursive: {reverse_recursive(numbers)}")
print(f"Reverse iterative: {reverse_iterative(numbers)}")
Python Output:
2^5 recursive: 32
2^5 iterative: 32
Count of 2 recursive: 3
Count of 2 iterative: 3
Reverse recursive: [5, 4, 3, 2, 1]
Reverse iterative: [5, 4, 3, 2, 1]
All conversions successful! Notice how the iterative versions follow the patterns we identified.
Project: Implement Same Algorithm Both Ways
Project: Implement Same Algorithm Both Ways
Now it's time to apply everything you've learned. You'll implement a non-trivial algorithm both recursively and iteratively, then compare them systematically.
Project Specification
Problem: Implement a function that finds all files matching a pattern in a directory tree.
Requirements:
- Search recursively through all subdirectories
- Match files by extension (e.g., ".py", ".txt")
- Return list of full paths to matching files
- Handle deep directory structures (> 1000 levels)
Deliverables:
- Recursive implementation
- Iterative implementation
- Performance comparison
- Analysis of trade-offs
Part 1: Recursive Implementation
import os
import time
def find_files_recursive(directory, extension):
"""
Find all files with given extension in directory tree.
Args:
directory: Root directory to search
extension: File extension to match (e.g., ".py")
Returns:
List of full paths to matching files
"""
matching_files = []
try:
# Process all items in current directory
for item in os.listdir(directory):
item_path = os.path.join(directory, item)
if os.path.isfile(item_path):
# Base case: it's a file, check if it matches
if item_path.endswith(extension):
matching_files.append(item_path)
elif os.path.isdir(item_path):
# Recursive case: it's a directory, search it
matching_files.extend(
find_files_recursive(item_path, extension)
)
except PermissionError:
# Skip directories we can't access
pass
return matching_files
# Test the recursive version
print("=== Recursive Implementation ===")
start = time.time()
python_files = find_files_recursive(".", ".py")
elapsed = time.time() - start
print(f"Found {len(python_files)} Python files")
print(f"Time: {elapsed:.4f} seconds")
print(f"\nFirst 5 files:")
for f in python_files[:5]:
print(f" {f}")
Python Output (example):
=== Recursive Implementation ===
Found 47 Python files
Time: 0.0234 seconds
First 5 files:
./module1/example1.py
./module1/example2.py
./module2/utils.py
./module2/tests/test_utils.py
./module3/main.py
Recursive solution analysis:
Strengths: - Natural problem decomposition - Clear and readable - Mirrors directory structure - Easy to understand and maintain
Weaknesses: - Subject to recursion limit - Stack space grows with depth - May fail on very deep trees
Part 2: Iterative Implementation
def find_files_iterative(directory, extension):
"""
Find all files with given extension in directory tree (iterative).
Args:
directory: Root directory to search
extension: File extension to match (e.g., ".py")
Returns:
List of full paths to matching files
"""
matching_files = []
# Stack of directories to process
dirs_to_process = [directory]
while dirs_to_process:
current_dir = dirs_to_process.pop()
try:
# Process all items in current directory
for item in os.listdir(current_dir):
item_path = os.path.join(current_dir, item)
if os.path.isfile(item_path):
# It's a file, check if it matches
if item_path.endswith(extension):
matching_files.append(item_path)
elif os.path.isdir(item_path):
# It's a directory, add to stack for processing
dirs_to_process.append(item_path)
except PermissionError:
# Skip directories we can't access
pass
return matching_files
# Test the iterative version
print("\n=== Iterative Implementation ===")
start = time.time()
python_files = find_files_iterative(".", ".py")
elapsed = time.time() - start
print(f"Found {len(python_files)} Python files")
print(f"Time: {elapsed:.4f} seconds")
print(f"\nFirst 5 files:")
for f in python_files[:5]:
print(f" {f}")
Python Output (example):
=== Iterative Implementation ===
Found 47 Python files
Time: 0.0198 seconds
First 5 files:
./module1/example1.py
./module1/example2.py
./module2/utils.py
./module2/tests/test_utils.py
./module3/main.py
Iterative solution analysis:
Strengths: - No recursion limit - Handles arbitrary depth - Slightly faster (no function call overhead) - Production-ready
Weaknesses: - More verbose - Explicit stack management - Less intuitive structure
Part 3: Performance Comparison
Let's compare both implementations systematically:
import os
import time
import sys
def benchmark_both(directory, extension, runs=5):
"""
Benchmark both implementations.
Args:
directory: Directory to search
extension: File extension to match
runs: Number of runs to average
"""
print(f"\n{'='*60}")
print(f"Benchmarking: {directory}")
print(f"Extension: {extension}")
print(f"Runs: {runs}")
print(f"{'='*60}\n")
# Warm-up run (to cache file system)
find_files_recursive(directory, extension)
# Benchmark recursive
recursive_times = []
for _ in range(runs):
start = time.time()
recursive_result = find_files_recursive(directory, extension)
recursive_times.append(time.time() - start)
# Benchmark iterative
iterative_times = []
for _ in range(runs):
start = time.time()
iterative_result = find_files_iterative(directory, extension)
iterative_times.append(time.time() - start)
# Calculate statistics
recursive_avg = sum(recursive_times) / len(recursive_times)
iterative_avg = sum(iterative_times) / len(iterative_times)
# Results
print(f"Files found: {len(recursive_result)}")
print(f"\nRecursive:")
print(f" Average time: {recursive_avg:.6f} seconds")
print(f" Min time: {min(recursive_times):.6f} seconds")
print(f" Max time: {max(recursive_times):.6f} seconds")
print(f"\nIterative:")
print(f" Average time: {iterative_avg:.6f} seconds")
print(f" Min time: {min(iterative_times):.6f} seconds")
print(f" Max time: {max(iterative_times):.6f} seconds")
print(f"\nSpeedup: {recursive_avg / iterative_avg:.2f}x")
# Verify results match
assert set(recursive_result) == set(iterative_result), "Results don't match!"
print("\nβ Both implementations produce identical results")
# Run benchmark
benchmark_both(".", ".py", runs=10)
Python Output (example):
============================================================
Benchmarking: .
Extension: .py
Runs: 10
============================================================
Files found: 47
Recursive:
Average time: 0.023456 seconds
Min time: 0.022134 seconds
Max time: 0.025678 seconds
Iterative:
Average time: 0.019823 seconds
Min time: 0.018901 seconds
Max time: 0.021234 seconds
Speedup: 1.18x
β Both implementations produce identical results
Part 4: Deep Directory Test
Let's test with a very deep directory structure:
def create_deep_test_structure(depth=1500):
"""Create a very deep directory structure for testing."""
root = "deep_test_project"
os.makedirs(root, exist_ok=True)
current = root
for i in range(depth):
current = os.path.join(current, f"level_{i}")
os.makedirs(current, exist_ok=True)
# Create a Python file at each level
file_path = os.path.join(current, f"module_{i}.py")
with open(file_path, "w") as f:
f.write(f"# Module at level {i}\n")
return root
print("\n=== Deep Directory Test ===")
print("Creating directory structure with depth 1500...")
root = create_deep_test_structure(1500)
# Test recursive (will fail)
print("\nTesting recursive implementation:")
try:
start = time.time()
files = find_files_recursive(root, ".py")
elapsed = time.time() - start
print(f"Success! Found {len(files)} files in {elapsed:.4f} seconds")
except RecursionError as e:
print(f"Failed with RecursionError: {e}")
print(f"Maximum recursion depth: {sys.getrecursionlimit()}")
# Test iterative (will succeed)
print("\nTesting iterative implementation:")
try:
start = time.time()
files = find_files_iterative(root, ".py")
elapsed = time.time() - start
print(f"Success! Found {len(files)} files in {elapsed:.4f} seconds")
except RecursionError as e:
print(f"Failed with RecursionError: {e}")
Python Output:
=== Deep Directory Test ===
Creating directory structure with depth 1500...
Testing recursive implementation:
Failed with RecursionError: maximum recursion depth exceeded
Maximum recursion depth: 1000
Testing iterative implementation:
Success! Found 1500 files in 0.3421 seconds
Critical insight: The iterative version handles arbitrary depth, while the recursive version fails on deep structures.
Part 5: Comprehensive Analysis
Let's create a complete comparison table:
def analyze_implementations():
"""Comprehensive analysis of both implementations."""
analysis = """
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β IMPLEMENTATION COMPARISON β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ASPECT β RECURSIVE β ITERATIVE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Code Clarity β β
β
β
β
β
Excellent β β
β
β
ββ Good β
β Readability β β
β
β
β
β
Very clear β β
β
β
ββ Moderate β
β Maintainability β β
β
β
β
β
Easy β β
β
β
β
β Moderate β
β Performance β β
β
β
β
β Good β β
β
β
β
β
Excellent β
β Memory Efficiency β β
β
β
ββ Moderate β β
β
β
β
β Good β
β Depth Handling β β
β
βββ Limited β β
β
β
β
β
Unlimited β
β Production Ready β β
β
β
ββ Conditional β β
β
β
β
β
Yes β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β OVERALL SCORE β 29/35 (83%) β 31/35 (89%) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
DETAILED ANALYSIS:
1. CODE CLARITY
Recursive: Natural problem decomposition, mirrors directory structure
Iterative: Requires understanding of explicit stack pattern
2. PERFORMANCE
Recursive: ~20-30ms for typical projects
Iterative: ~15-25ms for typical projects
Difference: ~15-20% faster (iterative)
3. MEMORY USAGE
Recursive: O(d) call stack where d = max depth
Iterative: O(d) explicit stack where d = max depth
Note: Similar space complexity, but iterative uses heap not stack
4. DEPTH HANDLING
Recursive: Limited to ~1000 levels (Python's recursion limit)
Iterative: Limited only by available memory (millions of levels)
5. EDGE CASES
Both handle:
- Empty directories
- Permission errors
- Symbolic links (if followed)
- Non-existent paths (with try/except)
RECOMMENDATIONS:
Use RECURSIVE when:
β Directory depth is known to be reasonable (< 100 levels)
β Code clarity is paramount
β Prototyping or educational context
β Team is more comfortable with recursive thinking
Use ITERATIVE when:
β Directory depth is unknown or potentially deep
β Production environment
β Performance is critical
β Need to handle arbitrary input safely
HYBRID APPROACH:
Consider using recursive for clarity, but add depth checking:
def find_files_safe(directory, extension, max_depth=100):
def recurse(dir, depth):
if depth > max_depth:
raise ValueError(f"Exceeded max depth {max_depth}")
# ... recursive logic with depth tracking
return recurse(directory, 0)
This gives you clarity with safety.
"""
print(analysis)
analyze_implementations()
Python Output:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β IMPLEMENTATION COMPARISON β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ASPECT β RECURSIVE β ITERATIVE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Code Clarity β β
β
β
β
β
Excellent β β
β
β
ββ Good β
β Readability β β
β
β
β
β
Very clear β β
β
β
ββ Moderate β
β Maintainability β β
β
β
β
β
Easy β β
β
β
β
β Moderate β
β Performance β β
β
β
β
β Good β β
β
β
β
β
Excellent β
β Memory Efficiency β β
β
β
ββ Moderate β β
β
β
β
β Good β
β Depth Handling β β
β
βββ Limited β β
β
β
β
β
Unlimited β
β Production Ready β β
β
β
ββ Conditional β β
β
β
β
β
Yes β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β OVERALL SCORE β 29/35 (83%) β 31/35 (89%) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Part 6: Your Turn - Extensions
Now extend the project with these challenges:
Challenge 1: Add filtering by file size
def find_files_by_size(directory, extension, min_size=0, max_size=float('inf')):
"""
Find files matching extension and size constraints.
Args:
directory: Root directory
extension: File extension
min_size: Minimum file size in bytes
max_size: Maximum file size in bytes
Returns:
List of (path, size) tuples
"""
# TODO: Implement this (try both recursive and iterative)
pass
# Test your implementation
# results = find_files_by_size(".", ".py", min_size=1000, max_size=10000)
# print(f"Found {len(results)} files between 1KB and 10KB")
Challenge 2: Add pattern matching
import re
def find_files_by_pattern(directory, pattern):
"""
Find files matching a regex pattern.
Args:
directory: Root directory
pattern: Regex pattern to match filenames
Returns:
List of matching file paths
"""
# TODO: Implement this
# Hint: Use re.search(pattern, filename)
pass
# Test your implementation
# results = find_files_by_pattern(".", r"test_.*\.py$")
# print(f"Found {len(results)} test files")
Challenge 3: Add content searching
def find_files_containing(directory, extension, search_text):
"""
Find files containing specific text.
Args:
directory: Root directory
extension: File extension
search_text: Text to search for in file contents
Returns:
List of (path, line_number, line_content) tuples
"""
# TODO: Implement this
# Hint: Read each file and search line by line
pass
# Test your implementation
# results = find_files_containing(".", ".py", "def find_files")
# print(f"Found {len(results)} occurrences")
Project Summary
You've now implemented the same algorithm both recursively and iteratively, and you've seen:
- When recursion shines: Natural problem structure, clear decomposition
- When iteration shines: Performance, depth handling, production safety
- How to convert: Systematic patterns for transformation
- Trade-offs: Clarity vs control, elegance vs robustness
Key takeaways:
- Recursion is not always better: Choose based on problem characteristics
- Iteration is not always worse: Sometimes it's the right tool
- Both have their place: Understanding both makes you a better programmer
- Measure, don't guess: Benchmark to make informed decisions
The master's approach:
- Start with the most natural solution (often recursive for hierarchical problems)
- Measure performance and identify limitations
- Convert to iterative if needed for production
- Document the trade-offs for future maintainers
You now have the tools to make informed decisions about recursion vs iteration in your own projects.
Decision Framework: The Complete Guide
Decision Framework: The Complete Guide
Let's synthesize everything into a practical decision framework you can use in real projects.
The Decision Tree
START: Need to solve a problem
β
ββ Does the problem have hierarchical/recursive structure?
β (trees, nested data, divide-and-conquer, etc.)
β β
β ββ YES β Is recursion depth bounded and reasonable (< 100)?
β β β
β β ββ YES β Is code clarity more important than performance?
β β β β
β β β ββ YES β USE RECURSION β
β β β β (Natural, clear, maintainable)
β β β β
β β β ββ NO β Measure performance
β β β β
β β β ββ Recursive fast enough? β USE RECURSION β
β β β ββ Need optimization? β USE ITERATION
β β β
β β ββ NO (depth > 100 or unknown) β USE ITERATION β
β β (Avoid recursion limit issues)
β β
β ββ NO (linear/sequential problem)
β β
β ββ USE ITERATION β
β (Natural fit, better performance)
β
ββ Special cases:
β
ββ Mathematical definition is recursive?
β β Start with RECURSION, optimize if needed
β
ββ Backtracking/search problem?
β β RECURSION usually clearer
β
ββ Production system with unknown input?
β β ITERATION for safety
β
ββ Educational/prototype code?
β RECURSION for clarity
Quick Reference Table
| Scenario | Recommended Approach | Reasoning |
|---|---|---|
| Binary tree traversal | Recursion | Natural structure match |
| Directory tree (known depth) | Recursion | Clear and maintainable |
| Directory tree (unknown depth) | Iteration | Safety from recursion limit |
| Merge sort | Recursion | Divide-and-conquer clarity |
| List sum/max/min | Iteration | Simple linear processing |
| Factorial/Fibonacci | Recursion (with memoization) | Mathematical definition |
| String reversal | Iteration (or slicing) | No natural decomposition |
| Graph DFS | Either | Depends on graph depth |
| Backtracking (N-Queens) | Recursion | State space exploration |
| File content search | Iteration | Sequential processing |
Complexity Considerations
Time Complexity: - Usually the same for both approaches - Iteration may be slightly faster (no function call overhead) - Difference typically < 20% in practice
Space Complexity: - Recursion: O(d) call stack where d = depth - Iteration: O(d) explicit stack where d = depth - Similar asymptotic complexity, but: - Recursive stack frames are larger (local vars, return address) - Iterative stack is more memory-efficient
When space matters: - Embedded systems β Prefer iteration - Deep recursion (> 1000) β Must use iteration - Moderate depth (< 100) β Either is fine
Production Checklist
Before deploying recursive code to production, verify:
- [ ] Maximum recursion depth is known and bounded
- [ ] Depth is well below Python's limit (< 500 recommended)
- [ ] Input validation prevents excessive depth
- [ ] Error handling for RecursionError (if applicable)
- [ ] Performance is acceptable under load
- [ ] Code is well-documented (explain recursive logic)
- [ ] Tests cover edge cases (empty input, max depth, etc.)
If any checkbox fails, consider iterative implementation.
The Pragmatic Approach
In practice, most professional developers:
- Start with the clearest solution (often recursive for hierarchical problems)
- Measure performance with realistic data
- Optimize if needed (convert to iteration if bottleneck)
- Document trade-offs for future maintainers
Example workflow:
# Version 1: Clear recursive implementation
def process_tree_v1(node):
"""Process tree recursively - clear but may hit limits."""
if node is None:
return 0
return 1 + process_tree_v1(node.left) + process_tree_v1(node.right)
# Version 2: Add depth checking for safety
def process_tree_v2(node, max_depth=1000):
"""Process tree with depth limit."""
def recurse(node, depth):
if depth > max_depth:
raise ValueError(f"Tree depth exceeds {max_depth}")
if node is None:
return 0
return 1 + recurse(node.left, depth+1) + recurse(node.right, depth+1)
return recurse(node, 0)
# Version 3: Iterative for production (if needed)
def process_tree_v3(node):
"""Process tree iteratively - production-safe."""
if node is None:
return 0
count = 0
stack = [node]
while stack:
current = stack.pop()
count += 1
if current.right:
stack.append(current.right)
if current.left:
stack.append(current.left)
return count
# Choose based on requirements:
# - v1 for prototypes and known-small trees
# - v2 for production with depth validation
# - v3 for production with unknown/deep trees
Common Mistakes to Avoid
Mistake 1: Using recursion for simple linear problems
# β Bad: Recursive sum (unnecessary complexity)
def sum_bad(lst):
if len(lst) == 0:
return 0
return lst[0] + sum_bad(lst[1:])
# β Good: Iterative sum (natural and efficient)
def sum_good(lst):
total = 0
for num in lst:
total += num
return total
# β Best: Built-in (idiomatic Python)
def sum_best(lst):
return sum(lst)
Mistake 2: Ignoring recursion depth in production
# β Bad: No depth checking
def process_directory_bad(path):
# Will fail on deep trees!
for item in os.listdir(path):
if os.path.isdir(item):
process_directory_bad(item)
# β Good: Depth checking
def process_directory_good(path, max_depth=100, depth=0):
if depth > max_depth:
raise ValueError(f"Directory depth exceeds {max_depth}")
for item in os.listdir(path):
if os.path.isdir(item):
process_directory_good(item, max_depth, depth+1)
# β Best: Iterative for unknown depth
def process_directory_best(path):
stack = [path]
while stack:
current = stack.pop()
for item in os.listdir(current):
if os.path.isdir(item):
stack.append(item)
Mistake 3: Premature optimization
# β Bad: Optimizing before measuring
# "I'll use iteration because it's faster"
def traverse_iterative(tree):
# Complex iterative code...
pass
# β Good: Start simple, measure, then optimize
def traverse_recursive(tree):
# Clear recursive code
pass
# Measure performance
# If too slow, THEN convert to iterative
Final Wisdom
The master's perspective:
"Recursion is a tool, not a goal. Use it when it clarifies the solution. Avoid it when it obscures the solution. Measure when in doubt."
Three questions to ask:
- Does recursion make the code clearer?
- If yes β Use recursion
-
If no β Use iteration
-
Is recursion depth bounded and reasonable?
- If yes β Recursion is safe
-
If no β Use iteration
-
Does performance matter here?
- If no β Choose for clarity
- If yes β Measure both, choose faster
Remember:
- Recursion is not inherently better or worse than iteration
- Both are tools with different strengths
- The best choice depends on the specific problem
- When in doubt, start with clarity, optimize if needed
You now have a complete framework for making informed decisions about recursion vs iteration. Use it wisely!